const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');

const MAX_CHUNK_LENGTH = 16383; // must be multiple of 3

const tripletToBase64 = (num: number) =>
  LOOKUP[(num >> 18) & 0x3f] + LOOKUP[(num >> 12) & 0x3f] + LOOKUP[(num >> 6) & 0x3f] + LOOKUP[num & 0x3f];

class Uft7Base64 {
  public static decode(base64Str: string) {
    let iOut = 0;
    const arr = new Uint8Array(Math.ceil((base64Str.length * 3) / 4));

    for (let i = 0, len = base64Str.length, validBits = 0, bitsSoFar = 0; i < len; i++) {
      let bits: number;
      const c = base64Str.charCodeAt(i);
      if (c >= 0x41 && c <= 0x5a) {
        // [A-Z]
        bits = c - 0x41;
      } else if (c >= 0x61 && c <= 0x7a) {
        // [a-z]
        bits = c - 0x61 + 0x1a;
      } else if (c >= 0x30 && c <= 0x39) {
        // [0-9]
        bits = c - 0x30 + 0x34;
      } else if (c === 0x2b) {
        // +
        bits = 0x3e;
      } else if (c === 0x2f) {
        // /
        bits = 0x3f;
      } else if (c === 0x3d) {
        // =
        validBits = 0;
        continue;
      } else {
        continue;
      }
      bitsSoFar = (bitsSoFar << 6) | bits;
      validBits += 6;
      if (validBits >= 8) {
        validBits -= 8;
        arr[iOut++] = bitsSoFar >> validBits;
        if (validBits === 2) {
          bitsSoFar &= 0x03;
        } else if (validBits === 4) {
          bitsSoFar &= 0x0f;
        }
      }
    }
    return iOut < arr.length ? arr.subarray(0, iOut) : arr;
  }

  public static encode(data: any) {
    const len = data.length;
    const extraBytes = len % 3;
    let output = '';

    for (let i = 0, len2 = len - extraBytes; i < len2; i += MAX_CHUNK_LENGTH) {
      output += this.encodeChunk(data, i, i + MAX_CHUNK_LENGTH > len2 ? len2 : i + MAX_CHUNK_LENGTH);
    }

    if (extraBytes === 1) {
      const tmp = data[len - 1];
      output += LOOKUP[tmp >> 2];
      output += LOOKUP[(tmp << 4) & 0x3f];
      output += '==';
    } else if (extraBytes === 2) {
      const tmp = (data[len - 2] << 8) + data[len - 1];
      output += LOOKUP[tmp >> 10];
      output += LOOKUP[(tmp >> 4) & 0x3f];
      output += LOOKUP[(tmp << 2) & 0x3f];
      output += '=';
    }

    return output;
  }

  public static encodeChunk(uint8: any, start: number, end: number) {
    let output = '';
    for (let i = start; i < end; i += 3) {
      output += tripletToBase64((uint8[i] << 16) + (uint8[i + 1] << 8) + uint8[i + 2]);
    }
    return output;
  }
}

function encodeToUTF7(str: string) {
  const b = new Uint8Array(str.length * 2);
  for (let i = 0, bi = 0, len = str.length; i < len; i++) {
    const c = str.charCodeAt(i);
    b[bi++] = c >> 8;
    b[bi++] = c & 0xff;
  }
  return Uft7Base64.encode(b).replace(/=+$/, '');
}

function decodeFromUTF7(str: string) {
  const octets = Uft7Base64.decode(str);
  let output = '';

  for (let i = 0, len = octets.length; i < len; ) {
    output += String.fromCharCode((octets[i++] << 8) | octets[i++]);
  }
  return output;
}

export class UTF7 {
  public static imapEncode(str: string) {
    return str
      .replace(/&/g, '&-')
      .replace(/[^\x20-\x7e]+/g, (chunk) => '&' + (chunk === '&' ? '' : encodeToUTF7(chunk)).replace(/\//g, ',') + '-');
  }

  public static imapDecode(str: string) {
    return str.replace(/&([^-]*)-/g, (_, chunk) => (chunk === '' ? '&' : decodeFromUTF7(chunk.replace(/,/g, '/'))));
  }
}
